/**
 * 
 */
package neptune.manager.model.experiment;

import java.util.ArrayList;
import java.util.Map;

import neptune.manager.IsUser;
import neptune.manager.allocator.AllocationException;
import neptune.manager.allocator.Allocator;
import neptune.manager.allocator.DeallocationException;
import neptune.manager.allocator.TerminationException;
import neptune.manager.configurator.Configurator;
import neptune.manager.configurator.ConfiguratorException;
import neptune.manager.model.cluster.Cluster;
import neptune.manager.model.experiment.ExperimentState.StatesID;
import neptune.manager.model.topology.IPAddress;
import neptune.manager.model.topology.Node;
import neptune.manager.model.topology.Topology;
import neptune.manager.validator.Validator;
import neptune.manager.validator.ValidatorResult;
import neptune.manager.validator.Validator.Status;

/**
 * @author Roberto Bifulco [info@robertobifulco.it, robertobifulco.it]
 * 
 */
public class Experiment {

	public static class Roles {
		public static String NORMAL_USER = "NORMAL USER";
		public static String SUPER_USER = "SUPER USER";
	}

	/**
	 * Amministratore (creatore) dell'esperimento
	 */
	private String administrator;
	/**
	 * Utenti con permessi di "lettura"
	 */
	private ArrayList<IsUser> users = new ArrayList<IsUser>(0);
	/**
	 * Gli utenti "privilegiati" di questo esperimento
	 */
	private ArrayList<IsUser> privilegedUsers = new ArrayList<IsUser>(0);
	/**
	 * La topologia associata all'esperimento
	 */
	private Topology topology;
	/**
	 * Identificativo (nome) univoco dell'esperimento
	 */
	private String experimentId;
	/**
	 * Oggetto che rappresenta l'attuale stato dell'esperimento
	 */
	private ExperimentState state;
	/**
	 * Rappresenta il range di indirizzi IP assegnati all'esperimento
	 */
	private IPAddress assignedSubnet;

	/**
	 * @param administrator
	 * @param experimentId
	 */
	public Experiment(String administrator, String experimentId) {
		this.administrator = administrator;
		this.experimentId = experimentId;

		state = new ExperimentState();
	}

	/**
	 * @return the administrator
	 */
	public String getAdministrator() {
		return administrator;
	}

	/**
	 * @param administrator
	 *            the administrator to set
	 */
	public void setAdministrator(String administrator) {
		this.administrator = administrator;
	}

	/**
	 * @return the users
	 */
	public IsUser[] getUsers() {
		return (IsUser[]) users.toArray(new IsUser[0]);
	}

	/**
	 * @param users
	 *            the users to set
	 */
	public void addUser(IsUser user) {
		this.users.add(user);
	}

	/**
	 * 
	 * @param username
	 */
	public void removeUser(String username) {
		for (IsUser user : users) {
			if (user.getUsername().equals(username)) {
				users.remove(user);
				return;
			}
		}
	}

	/**
	 * @return the privilegedUsers
	 */
	public IsUser[] getPrivilegedUsers() {
		return (IsUser[]) privilegedUsers.toArray(new IsUser[0]);
	}

	/**
	 * @param privilegedUsers
	 *            the privilegedUsers to set
	 */
	public void addPrivilegedUser(IsUser privilegedUser) {
		this.privilegedUsers.add(privilegedUser);
	}

	/**
	 * @return the topology
	 */
	public Topology getTopology() {
		if (topology == null) {
			// Se la topologia non e' definita, aggiunge comunque una topologia
			// vuota.
			topology = new Topology();
			topology.setName(experimentId);
		}

		return topology;
	}

	/**
	 * Imposta la topologia per questo esperimento. Eseguendo gli opportuni
	 * controlli con il validator passato come argomento, ed aggiornandone lo
	 * stato.
	 * 
	 * @param topology
	 *            the topology to set
	 */
	public ValidatorResult setTopology(Topology topology,
			Validator<Topology> validator) throws TopologyException {

		if (!state.getValue().equals(ExperimentState.StatesID.UNDEFINED_LABEL)
				&& !state.getValue().equals(
						ExperimentState.StatesID.DEFINED_LABEL)) {
			throw new TopologyException(
					"Experiment topology modifications allowed only if "
							+ "the experiment state is UNDEFINED or DEFINED");
		}

		ValidatorResult result = validator.validate(topology,
				new ValidatorResult("Topology-" + experimentId + "-Validator"));

		if (result.getStatus() == Status.VALID) {
			if (state.getValue().equals(
					ExperimentState.StatesID.UNDEFINED_LABEL))
				state.setValue(ExperimentState.StatesID.DEFINED_LABEL);

			this.topology = topology;
		}

		return result;
	}

	/**
	 * Non effettua nessun controllo sulla topologia passata e, di conseguenza,
	 * non aggiorna lo stato dell'epserimento.
	 * 
	 * @param topology
	 */
	public void setTopologyWithoutControls(Topology topology) {
		this.topology = topology;
	}

	/**
	 * @return the experimentId
	 */
	public String getExperimentId() {
		return experimentId;
	}

	/**
	 * @param experimentId
	 *            the experimentId to set
	 */
	public void setExperimentId(String experimentId) {
		this.experimentId = experimentId;
	}

	/**
	 * @return the state
	 */
	public ExperimentState getState() {
		// verifyState();
		return state;
	}

	public Node[] getHosterNodes() {
		// TODO verificare correttezza

		if (topology == null) {
			return null;
		}

		Node[] nodes = topology.getNodes();
		ArrayList<Node> hosterNodes = new ArrayList<Node>(1);
		for (int i = 0; i < nodes.length; i++) {
			if (nodes[i].getHoster() != null) {
				if (!hosterNodes.contains(nodes[i].getHoster()))
					hosterNodes.add(nodes[i].getHoster());
			}
		}

		return hosterNodes.toArray(new Node[1]);

	}

	/**
	 * Avvia il processo di configurazione dei nodi della topologia
	 * dell'esperimento sui nodi fisici del cluster. (Caso d'uso Experiment Set
	 * Up). I nodi vengono sostanzialmente posti nello stato di avvio, e vengono
	 * installati tutti i software necessari.
	 * 
	 * @throws ConfiguratorException
	 */
	public void setUp(Configurator configurator) throws ConfiguratorException {
		// TODO Si suppone che se l'esperimento e' nella fase di SETTING_UP,
		// allora,
		// e' gia' stato allocato e tutti i nodi facenti parte della topologia
		// hanno impostato un indirizzo IP di controllo
		if (state.getValue().equals(ExperimentState.StatesID.ALLOCATED_LABEL)
				|| state.getValue().equals(
						ExperimentState.StatesID.RUNNING_LABEL)) {
			configurator.configureTopology(this.topology);
			state.setValue(ExperimentState.StatesID.RUNNING_LABEL);
			state.setStartTimestamp(System.currentTimeMillis());
		} else
			throw new ConfiguratorException("Experiment '" + this.experimentId
					+ "' state is not " + StatesID.ALLOCATED_LABEL + " or "
					+ StatesID.RUNNING_LABEL
					+ ". Only experiments in those states can be configured.");
	}

	/**
	 * Avvia il processo di swap in dell'esperimento, con creazione delle
	 * macchine virtuali sulle macchine fisiche del cluster.
	 * 
	 * @throws AllocationException
	 */
	public void swapIn(Allocator allocator, Cluster cluster)
			throws AllocationException {

		if (state.getValue().equals(ExperimentState.StatesID.UNDEFINED_LABEL))
			throw new AllocationException(
					"Experiments in UNDEFINED state are not allocable.");
		if (state.getValue().equals(ExperimentState.StatesID.ALLOCATED)
				|| state.getValue().equals(ExperimentState.StatesID.RUNNING))
			throw new AllocationException(
					"The experiment is already allocated, "
							+ "to make a new allocation, you need to swap it out first.");

		// TODO esecuzione degli opportuni controlli!
		allocator.allocate(experimentId, topology, cluster);
		state.setValue(ExperimentState.StatesID.ALLOCATED_LABEL);
	}

	/**
	 * Avvia il processo di swap in dell'esperimento, con creazione delle
	 * macchine virtuali sulle macchine fisiche del cluster.
	 * 
	 * @throws AllocationException
	 */
	public void swapIn(Allocator allocator, Cluster cluster,
			Map<String, String[]> allocationMap) throws AllocationException {

		if (state.getValue().equals(ExperimentState.StatesID.UNDEFINED_LABEL))
			throw new AllocationException(
					"Experiments in UNDEFINED state are not allocable.");
		if (state.getValue().equals(ExperimentState.StatesID.ALLOCATED)
				|| state.getValue().equals(ExperimentState.StatesID.RUNNING))
			throw new AllocationException(
					"The experiment is already allocated, "
							+ "to make a new allocation, you need to swap it out first.");

		// TODO esecuzione degli opportuni controlli!
		allocator.allocateWithAllocationMap(allocationMap, topology, cluster);
		state.setValue(ExperimentState.StatesID.ALLOCATED_LABEL);
	}

	/**
	 * Dealloca l'esperimento dal cluster, salvandone opportunamente lo stato
	 * 
	 * @throws DeallocationException
	 */
	public void swapOut(Allocator allocator, Cluster cluster)
			throws DeallocationException {
		// TODO realizzare
		allocator.deallocate(experimentId, topology, cluster);
		state.setValue(ExperimentState.StatesID.DEFINED_LABEL);
		state.setStopTimestamp(System.currentTimeMillis());

		long totalExecutionTime = state.getTotalExecutionTime();
		totalExecutionTime += state.getStopTimestamp()
				- state.getStartTimestamp();

		state.setTotalExecutionTime(totalExecutionTime);
	}

	/**
	 * Avvia l'esecuzione dell'esperimento. Il processo di avvio dell'esecuzione
	 * corrisponde all'effettuare nell'ordine:<br>
	 * - SwapIn<br>
	 * - SetUp<br>
	 * La chiamata a questo metodo non fa altro che eseguire in sequenza i due
	 * precedenti metodi, verificando il risultato degli stessi.
	 * 
	 * @throws AllocationException
	 * @throws ConfiguratorException
	 */
	public void startUp(Allocator allocator, Cluster cluster,
			Configurator configurator, Validator<Topology> validator)
			throws AllocationException, ConfiguratorException {
		// TODO
		swapIn(allocator, cluster);
		setUp(configurator);
	}

	/**
	 * Ferma l'esecuzione dell'esperimento, deallocandolo dal server. Questa
	 * operazione non salva lo stato attuale dell'esperimento, poiche' si
	 * suppone non verra' piu' riattivato.<br>
	 * <br>
	 * E' l'operazione che viene chiamata quando l'esperimento deve cessare ogni
	 * sua attivita'.
	 * 
	 * @throws DeallocationException
	 */
	public void terminate(Allocator allocator, Cluster cluster)
			throws TerminationException {
		// TODO LUIGI realizzare
		//allocator.deallocate(experimentId, topology, cluster);
		allocator.terminate(experimentId, topology, cluster);
		state.setValue(ExperimentState.StatesID.UNDEFINED_LABEL);
	}

	/**
	 * Colleziona le informazioni di log appartenenti ai vari nodi
	 * dell'esperimento
	 */
	public ExperimentLog getExperimentData() {
		// TODO realizzare
		return null;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		String string = "Experiment id: " + this.getExperimentId() + "\n"
				+ "Experiment admin: " + this.getAdministrator() + "\n"
				+ "Experiment state: " + state;
		if (state.getValue().equals(ExperimentState.StatesID.ALLOCATED_LABEL)
				|| state.getValue().equals(
						ExperimentState.StatesID.RUNNING_LABEL)) {
			string += "\n   Hoster nodes:\n";
			for (int i = 0; i < getHosterNodes().length; i++) {
				string += "   " + getHosterNodes()[i].getId() + "\n";
			}
		}
		return string;
	}

	/**
	 * Gets an ArrayList of super users
	 * 
	 * @return
	 */
	public ArrayList<IsUser> getPrivilegedUserList() {
		return privilegedUsers;
	}

	/**
	 * Gets an ArrayList of normal users
	 * 
	 * @return
	 */
	public ArrayList<IsUser> getUserList() {
		return users;
	}

	/**
	 * Convalida la topologia associata a questo esperimento, utilizzando il
	 * {@link Validator} passato come argomento.
	 * 
	 * @param validator
	 * @return
	 */
	public ValidatorResult validate(Validator<Topology> validator) {
		ValidatorResult result = validator
				.validate(topology, new ValidatorResult("Experiment-"
						+ experimentId + "-Validator"));

		if (state.getValue().equals(ExperimentState.StatesID.UNDEFINED_LABEL))
			if (result.getStatus() == Status.VALID)
				state.setValue(ExperimentState.StatesID.DEFINED_LABEL);

		if (state.getValue().equals(ExperimentState.StatesID.DEFINED_LABEL))
			if (result.getStatus() == Status.ERROR)
				state.setValue(ExperimentState.StatesID.UNDEFINED_LABEL);

		return result;
	}

	/**
	 * Restituisce la subnet assegnata a questo esperimento
	 * 
	 * @return the assignedSubnet
	 */
	public IPAddress getAssignedSubnet() {
		return assignedSubnet;
	}

	/**
	 * Imposta la subnet i cui indirizzi sono quelli gestiti da questo
	 * esperimento.
	 * 
	 * @param assignedSubnet
	 *            the assignedSubnet to set
	 */
	public void setAssignedSubnet(IPAddress assignedSubnet) {
		this.assignedSubnet = assignedSubnet;
	}
}
